Ex1 - ImageNet

1. What is ImageNet?

The following text is a brief summary of information found on ImageNet about page:

ImageNet is an ongoing research effort to provide researchers around the world an easily accessible image database. It consists in an image dataset organized according to the WordNet hierarchy. Each meaningful concept in WordNet, potentially described by multiple words or word phrases, is called a “synonym set” or “synset”. There are more than 100,000 synsets in WordNet, majority of them are nouns (80,000+). ImageNet, aims at providing images to illustrate each synset. On average there is 1000 images per synset, and each of them was quality-controlled and human-annotated.

2. How many different kinds of cheese can you find in ImageNet?

You can find 37 different kinds of cheese on ImageNet, but are cream cheese and triple cream really cheese? Similarly grated cheese (“fromage râpé”) can be any kind of cheese. So let’s say that there is 37 subcategories of cheese rather than 37 kinds ;)

3. What is the best classifier on ImageNet and what is its error rate?

In December 2017, Google’s NASNet became the best Classifier on ImageNet, even though it was not part of any official ImageNet challenge. The model achieves a new performance with Top 1 Accuracy of 82.70% and Top 5 Accuracy of 96.20%.

Ex. 2 — Build an image recognition system

  1. Import Keras
library(keras)
library(dplyr)
library(imager)
library(base)
  1. Define ResNet50 as you model and check its architecture
model <- application_resnet50(weights = 'imagenet')
2018-03-13 15:45:09.727986: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
#summary(model)
plot_model(model)
Error in plot_model(model) : 
  impossible de trouver la fonction "plot_model"

For a better vizualisation of the architecture of this 50-layer residual neural network, we tried to use plot_model(model) but it didn’t work (even if it’s supposed to be implemented). This is what we obtained using the function in a Jupyter notebook (we only kept the first and last layers):

  1. Open an image representing a single object (if possible represented in ImageNet):

We try with a picture of emmental cheese :

img_path <- "Img/Cheese.jpg"
img <- image_load(img_path, target_size = c(224,224))
plot(load.image(img_path))

  1. Reshape the image to fit the input format of your model
x <- image_to_array(img)
x <- array_reshape(x, c(1, dim(x)))
  1. Preprocess the input
x <- imagenet_preprocess_input(x)
  1. Get the model predictions
preds <- model %>% predict(x)
  1. Display the top 5 recognized objects. Do you find the one represented on your image?
imagenet_decode_predictions(preds, top = 5)[[1]]

Conclusion : The NN didn’t manage to recognize this slice of cheese.

However if we try with a picture of a Golden retriever we obtain excellent results:

img_path <- "Img/Dog.jpg"
img <- image_load(img_path, target_size = c(224,224))
plot(load.image(img_path))

x <- image_to_array(img)
x <- array_reshape(x, c(1, dim(x)))
x <- imagenet_preprocess_input(x)
preds <- model %>% predict(x)
imagenet_decode_predictions(preds, top = 5)[[1]]

Ex. 3 — Your turn

  1. Based on your previous work, build an binary object recognition (only two objects) by transfer learning and fine tuning.

a) choose two classes (cat/dog or muffin/chihuahua or parrot/guacamole or livarot/pont leveque or whatever/whatever else…)

We’re going to build a hotdog - salad classifier. Actually we were inspired by Silicon Valley’s “Hotdog/Not hotdog classifier”

“What would you say if I told you there is a app on the market that tell you if you have a hotdog or not a hotdog. It is very good and I do not want to work on it any more. You can hire someone else.” - Jian-Yang, Silicon Valley, 2017

b) download some (say 10 to 50) images of each class on the web split your images into two sets (training and testing) and setup our data with a training directory and a validation directory as follows:

We downloaded from ImageNet a list of urls leading to pictures of salads and hotdogs.

dir.create("salad")
dir.create("hotdog")
download.file("http://www.image-net.org/api/text/imagenet.synset.geturls?wnid=n07806774","salad/salad.txt", mode = 'wb')
download.file("http://www.image-net.org/api/text/imagenet.synset.geturls?wnid=n07697537","hotdog/hotdog.txt", mode = 'wb')
salad_url = read.table("salad/salad.txt")
salad_url$V1<- as.character(salad_url$V1)
hotdog_url = read.table("hotdog/hotdog.txt")
hotdog_url$V1<- as.character(hotdog_url$V1)

Let’s download 300 pictures of salads and 300 pictures of hotdogs

for (i in 1:300){
  tryCatch({
    download.file(salad_url[i,],paste("salad/salad_",i,".jpg",sep=""), mode = 'wb')
    },
    
    error=function(e){
      print("error")
    })
}

for (i in 1:300){
  tryCatch({
    download.file(hotdog_url[i,],paste("hotdog/hotdog_",i,".jpg",sep=""), mode = 'wb')
    },
    
    error=function(e){
      print("error")
    })
}
#

Some of the pictures were corrupted and some represented more than a simple item (e.g. a salad & a glass of water), so we removed them manually from the dataset.

At the end of the day we obtain :

f1 = list.files("hotdog",pattern=".jpg")
print(paste(length(f1), "hotdog pictures"))
f2 = list.files("salad",pattern=".jpg")
print(paste(length(f2), "salad pictures"))

In order not to favor any category, we’re going to keep only 64 salad pictures

f2=f2[1:min(length(f1),length(f2))]
f1=f1[1:min(length(f1),length(f2))]
print(length(f2)==length(f1))
print(length(f1))

We split our data into 80/20 and move it to train/test directories. The architecture of these directories is :

train_dir/ hotdog/ salad/ test_dir/ hotdog/ salad/

dir.create("train_dir")
dir.create("train_dir/hotdog")
dir.create("train_dir/salad")
dir.create("test_dir")
dir.create("test_dir/hotdog")
dir.create("test_dir/salad")
index_hotdog = sample(1:length(f1), round(0.8*length(f1)))

train_hotdog = f1[index_hotdog]
file.copy(from=paste("hotdog/",train_hotdog,sep=""),
          to=paste("train_dir/hotdog/",train_hotdog,sep=""))

test_hotdog = f1[-index_hotdog]
file.copy(from=paste("hotdog/",test_hotdog,sep=""),
          to=paste("test_dir/hotdog/",test_hotdog,sep=""))


index_salad = sample(1:length(f2), round(0.8*length(f2)))

train_salad = f2[index_salad]
file.copy(from=paste("salad/",train_salad,sep=""),
          to=paste("train_dir/salad/",train_salad,sep=""))

test_salad = f2[-index_salad]
file.copy(from=paste("salad/",test_salad,sep=""),
          to=paste("test_dir/salad/",test_salad,sep=""))
#

c) training the network

We initially tried to use an Xception pre-trained neural net in order to build our model, but we fail to tune the hyperparameters (we also tried to add more data and try to augment it) … These are the final results we obtained with Xception :

So we moved to VGG 16 :

### Defining training and testing sets
f1 = list.files("hotdog",pattern=".jpg")
f2 = list.files("salad",pattern=".jpg")
img_width = 150
img_height = 150
batch_size = 10
train_directory = "train_dir"
test_directory = "test_dir"
 
train_generator = flow_images_from_directory(
                              train_directory, 
                              generator = image_data_generator(),
                              target_size = c(img_width, img_height),
                              color_mode = "rgb",
                              class_mode = "categorical", 
                              batch_size = batch_size,
                              shuffle=TRUE,
                              seed = 42 )
validation_generator = flow_images_from_directory(
                                    test_directory,
                                    generator = image_data_generator(),                                                                        target_size = c(img_width, img_height),
                                    color_mode = "rgb",
                                    classes = NULL,
                                    class_mode = "categorical",
                                    batch_size = batch_size,
                                    shuffle = TRUE,
                                    seed = 42)
train_samples = round(0.8*length(f1))
validation_samples = round(0.2*length(f1))
### Defining the pretrained model : VGG16
base_model <- application_vgg16(weights = 'imagenet',
                                include_top = FALSE, ## We drop the final layer (i.e. the prediction layer)
                                input_shape = c(img_width, img_height, 3))
 
### Adding one more layer
predictions <- base_model$output %>% 
                      layer_global_average_pooling_2d(trainable = T) %>% 
                      layer_dense(128, trainable = T) %>% #128 fully connected neurons
                      layer_activation("relu", trainable = T) %>% #ReLU activation function
                      layer_dropout(0.5, trainable = T) %>% #Dropping-out 50% of the  intermediary results 
                                                            #to avoid overfitting
                      layer_dense(2, trainable=T) %>% # We add 1 more layer with only 2 neuros = 2 classes
                      layer_activation("softmax", trainable=T) # The out_put function is softmax
 
### The model to train
model <- keras_model(inputs = base_model$input, outputs = predictions)
### Freezing all layers previously trained by VGG16
for (layer in base_model$layers){
  layer$trainable <- FALSE
}
  
 
### Let's do the maths
model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_rmsprop(lr = 0.005, decay = 1e-6), #learning rate of 0.005
  metrics = "accuracy"
)
 
results <- model %>% fit_generator(
                          train_generator,
                          steps_per_epoch = as.integer(train_samples/batch_size), 
                          epochs = 20, # 20 epochs
                          validation_data = validation_generator,
                          validation_steps = as.integer(validation_samples/batch_size),
                          verbose=2
)

Here are the results we obtained. At the best of the learning process we achieved 0.96 of accuracy and 0.97 of validation accuracy.

plot(results)

Bonus: This is the chunk of code we used to augment the data when Xception wouldn’t work …


### Generating "new" data out of the original data by rotating/translating/croping/resizing/... it

datagen <- image_data_generator(
  rotation_range = 20,
  width_shift_range = 0.2,
  height_shift_range = 0.2,
  horizontal_flip = TRUE
)
 
train_augmented_generator <-  flow_images_from_directory(test_directory, 
                                                         generator = datagen,
                                                         target_size = c(img_width, img_height), 
                                                         color_mode = "rgb", 
                                                         classes = NULL, 
                                                         class_mode = "categorical", 
                                                         batch_size = batch_size, 
                                                         shuffle = TRUE,  
                                                         seed = 42)

### The model to train

model <- keras_model(inputs = base_model$input, outputs = predictions)

for (layer in base_model$layers){
  layer$trainable <- FALSE
}


### Training on the augmented data

result_aug <- model %>% fit_generator(
  train_augmented_generator,
  steps_per_epoch = as.integer(train_samples/batch_size), 
  epochs = 20, 
  validation_data = validation_generator,
  validation_steps = as.integer(validation_samples/batch_size),
  verbose=2
)

Let’s visualize the results of our model

We test our model on :

img_list = c("hotdog.png","salad.jpg","snap.png","hotdog_salad.jpg")
img_path_list = paste("Img/",img_list,sep="")
for (img_path in img_path_list){
  img <- image_load(img_path, target_size = c(150,150))
  plot(load.image(img_path))
  x <- image_to_array(img)
  x <- array_reshape(x, c(1, dim(x)))
  x <- imagenet_preprocess_input(x)
  preds <- model %>% predict(x)
  if (preds[,1]==1){
    print("Hotdog")
  } else {
    print("Salad")
  }
}
[1] "Hotdog"

[1] "Salad"

[1] "Hotdog"

[1] "Salad"

As expected we predicted well on regular usecases. Yet in the tricky case of a salad + a hotdog our classifier predicted a salad rather than a hotdog.

  1. Is it better to do transfer learning and fine tuning or both?

We obtained these few guidelines from Deep Learning Sandbox

On our way to make money !

LS0tCnRpdGxlOiAiSG9tZXdvcmsgb24gbmV1cmFsIG5ldHdvcmtzIC0gSG90ZG9nIG9yIG5vdCBob3Rkb2cgPyAiCmF1dGhvcjogIlNhY2hhIElaQURJIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgojIEV4MSAtIEltYWdlTmV0CgoqKjEuIFdoYXQgaXMgSW1hZ2VOZXQ/KioKClRoZSBmb2xsb3dpbmcgdGV4dCBpcyBhIGJyaWVmIHN1bW1hcnkgb2YgaW5mb3JtYXRpb24gZm91bmQgb24gSW1hZ2VOZXQgW2Fib3V0IHBhZ2VdKGh0dHA6Ly9pbWFnZS1uZXQub3JnL2Fib3V0LW92ZXJ2aWV3KToKCkltYWdlTmV0IGlzIGFuIG9uZ29pbmcgcmVzZWFyY2ggZWZmb3J0IHRvIHByb3ZpZGUgcmVzZWFyY2hlcnMgYXJvdW5kIHRoZSB3b3JsZCBhbiBlYXNpbHkgYWNjZXNzaWJsZSBpbWFnZSBkYXRhYmFzZS4gSXQgY29uc2lzdHMgaW4gYW4gaW1hZ2UgZGF0YXNldCBvcmdhbml6ZWQgYWNjb3JkaW5nIHRvIHRoZSBbV29yZE5ldF0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvV29yZE5ldCkgaGllcmFyY2h5LiBFYWNoIG1lYW5pbmdmdWwgY29uY2VwdCBpbiBXb3JkTmV0LCBwb3RlbnRpYWxseSBkZXNjcmliZWQgYnkgbXVsdGlwbGUgd29yZHMgb3Igd29yZCBwaHJhc2VzLCBpcyBjYWxsZWQgYSAic3lub255bSBzZXQiIG9yICJzeW5zZXQiLiBUaGVyZSBhcmUgbW9yZSB0aGFuIDEwMCwwMDAgc3luc2V0cyBpbiBXb3JkTmV0LCBtYWpvcml0eSBvZiB0aGVtIGFyZSBub3VucyAoODAsMDAwKykuIEltYWdlTmV0LCBhaW1zIGF0IHByb3ZpZGluZyBpbWFnZXMgdG8gaWxsdXN0cmF0ZSBlYWNoIHN5bnNldC4gT24gYXZlcmFnZSB0aGVyZSBpcyAxMDAwIGltYWdlcyBwZXIgc3luc2V0LCBhbmQgZWFjaCBvZiB0aGVtIHdhcyBxdWFsaXR5LWNvbnRyb2xsZWQgYW5kIGh1bWFuLWFubm90YXRlZC4gCgoKKioyLiBIb3cgbWFueSBkaWZmZXJlbnQga2luZHMgb2YgY2hlZXNlIGNhbiB5b3UgZmluZCBpbiBJbWFnZU5ldD8qKgogCllvdSBjYW4gZmluZCAzNyBkaWZmZXJlbnQga2luZHMgb2YgY2hlZXNlIG9uIEltYWdlTmV0LCBidXQgYXJlIGNyZWFtIGNoZWVzZSBhbmQgdHJpcGxlIGNyZWFtIHJlYWxseSBjaGVlc2U/IFNpbWlsYXJseSBncmF0ZWQgY2hlZXNlICgiZnJvbWFnZSByw6Jww6kiKSBjYW4gYmUgYW55IGtpbmQgb2YgY2hlZXNlLiBTbyBsZXQncyBzYXkgdGhhdCB0aGVyZSBpcyAzNyBzdWJjYXRlZ29yaWVzIG9mIGNoZWVzZSByYXRoZXIgdGhhbiAzNyBraW5kcyA7KQogCioqMy4gV2hhdCBpcyB0aGUgYmVzdCBjbGFzc2lmaWVyIG9uIEltYWdlTmV0IGFuZCB3aGF0IGlzIGl0cyBlcnJvciByYXRlPyoqCiAKIApJbiBEZWNlbWJlciAyMDE3LCBHb29nbGUncyBbTkFTTmV0XShodHRwczovL2FyeGl2Lm9yZy9wZGYvMTcwNy4wNzAxMi5wZGYpIGJlY2FtZSB0aGUgYmVzdCBDbGFzc2lmaWVyIG9uIEltYWdlTmV0LCBldmVuIHRob3VnaCBpdCB3YXMgbm90IHBhcnQgb2YgYW55IG9mZmljaWFsIEltYWdlTmV0IGNoYWxsZW5nZS4gVGhlIG1vZGVsIGFjaGlldmVzIGEgbmV3IHBlcmZvcm1hbmNlIHdpdGggVG9wIDEgQWNjdXJhY3kgb2YgODIuNzAlIGFuZCBUb3AgNSBBY2N1cmFjeSBvZiA5Ni4yMCUuCiAKIAogCiMgRXguIDIg4oCUIEJ1aWxkIGFuIGltYWdlIHJlY29nbml0aW9uIHN5c3RlbQoKMS4gSW1wb3J0IEtlcmFzCmBgYHtyfQpsaWJyYXJ5KGtlcmFzKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGltYWdlcikKbGlicmFyeShiYXNlKQpgYGAKCjIuIERlZmluZSBSZXNOZXQ1MCBhcyB5b3UgbW9kZWwgYW5kIGNoZWNrIGl0cyBhcmNoaXRlY3R1cmUKYGBge3J9Cm1vZGVsIDwtIGFwcGxpY2F0aW9uX3Jlc25ldDUwKHdlaWdodHMgPSAnaW1hZ2VuZXQnKQojc3VtbWFyeShtb2RlbCkKYGBgCgpgYGB7cn0KcGxvdF9tb2RlbChtb2RlbCkKYGBgCgoKRm9yIGEgYmV0dGVyIHZpenVhbGlzYXRpb24gb2YgdGhlIGFyY2hpdGVjdHVyZSBvZiB0aGlzIDUwLWxheWVyIHJlc2lkdWFsIG5ldXJhbCBuZXR3b3JrLCB3ZSB0cmllZCB0byB1c2UgYHBsb3RfbW9kZWwobW9kZWwpYCBidXQgaXQgZGlkbid0IHdvcmsgKGV2ZW4gaWYgaXQncyBzdXBwb3NlZCB0byBiZSBbaW1wbGVtZW50ZWRdKGh0dHBzOi8vcmRyci5pby9jcmFuL2tlcmFzUi9tYW4vcGxvdF9tb2RlbC5odG1sKSkuIFRoaXMgaXMgd2hhdCB3ZSBvYnRhaW5lZCB1c2luZyB0aGUgZnVuY3Rpb24gaW4gYSBKdXB5dGVyIG5vdGVib29rICh3ZSBvbmx5IGtlcHQgdGhlIGZpcnN0IGFuZCBsYXN0IGxheWVycyk6Cgo8ZGl2IGlkPSJiZyI+CiAgPGltZyBzcmM9IkltZy9tb2RlbDEucG5nIiB3aWR0aD0zMDA+IDxpbWcgc3JjPSJJbWcvbW9kZWwyLnBuZyIgd2lkdGg9MzAwPgo8L2Rpdj4gCgoKMy4gT3BlbiBhbiBpbWFnZSByZXByZXNlbnRpbmcgYSBzaW5nbGUgb2JqZWN0IChpZiBwb3NzaWJsZSByZXByZXNlbnRlZCBpbiBJbWFnZU5ldCk6CgpXZSB0cnkgd2l0aCBhIHBpY3R1cmUgb2YgZW1tZW50YWwgY2hlZXNlIDoKYGBge3J9CmltZ19wYXRoIDwtICJJbWcvQ2hlZXNlLmpwZyIKaW1nIDwtIGltYWdlX2xvYWQoaW1nX3BhdGgsIHRhcmdldF9zaXplID0gYygyMjQsMjI0KSkKCnBsb3QobG9hZC5pbWFnZShpbWdfcGF0aCkpCmBgYAoKNC4gUmVzaGFwZSB0aGUgaW1hZ2UgdG8gZml0IHRoZSBpbnB1dCBmb3JtYXQgb2YgeW91ciBtb2RlbApgYGB7cn0KeCA8LSBpbWFnZV90b19hcnJheShpbWcpCnggPC0gYXJyYXlfcmVzaGFwZSh4LCBjKDEsIGRpbSh4KSkpCmBgYAoKNS4gUHJlcHJvY2VzcyB0aGUgaW5wdXQKYGBge3J9CnggPC0gaW1hZ2VuZXRfcHJlcHJvY2Vzc19pbnB1dCh4KQpgYGAKCjYuIEdldCB0aGUgbW9kZWwgcHJlZGljdGlvbnMKYGBge3J9CnByZWRzIDwtIG1vZGVsICU+JSBwcmVkaWN0KHgpCmBgYAoKNy4gRGlzcGxheSB0aGUgdG9wIDUgcmVjb2duaXplZCBvYmplY3RzLiBEbyB5b3UgZmluZCB0aGUgb25lIHJlcHJlc2VudGVkIG9uIHlvdXIgaW1hZ2U/CmBgYHtyfQppbWFnZW5ldF9kZWNvZGVfcHJlZGljdGlvbnMocHJlZHMsIHRvcCA9IDUpW1sxXV0KYGBgCgpDb25jbHVzaW9uIDogVGhlIE5OIGRpZG4ndCBtYW5hZ2UgdG8gcmVjb2duaXplIHRoaXMgc2xpY2Ugb2YgY2hlZXNlLgoKSG93ZXZlciBpZiB3ZSB0cnkgd2l0aCBhIHBpY3R1cmUgb2YgYSBHb2xkZW4gcmV0cmlldmVyIHdlIG9idGFpbiBleGNlbGxlbnQgcmVzdWx0czoKYGBge3J9CmltZ19wYXRoIDwtICJJbWcvRG9nLmpwZyIKaW1nIDwtIGltYWdlX2xvYWQoaW1nX3BhdGgsIHRhcmdldF9zaXplID0gYygyMjQsMjI0KSkKcGxvdChsb2FkLmltYWdlKGltZ19wYXRoKSkKCnggPC0gaW1hZ2VfdG9fYXJyYXkoaW1nKQp4IDwtIGFycmF5X3Jlc2hhcGUoeCwgYygxLCBkaW0oeCkpKQoKeCA8LSBpbWFnZW5ldF9wcmVwcm9jZXNzX2lucHV0KHgpCgpwcmVkcyA8LSBtb2RlbCAlPiUgcHJlZGljdCh4KQoKaW1hZ2VuZXRfZGVjb2RlX3ByZWRpY3Rpb25zKHByZWRzLCB0b3AgPSA1KVtbMV1dCmBgYAoKCgojIEV4LiAzIOKAlCBZb3VyIHR1cm4KCjEuIEJhc2VkIG9uIHlvdXIgcHJldmlvdXMgd29yaywgYnVpbGQgYW4gYmluYXJ5IG9iamVjdCByZWNvZ25pdGlvbiAob25seSB0d28gb2JqZWN0cykgYnkgdHJhbnNmZXIgbGVhcm5pbmcgYW5kIGZpbmUgdHVuaW5nLgoKX2EpIGNob29zZSB0d28gY2xhc3NlcyAoY2F0L2RvZyBvciBtdWZmaW4vY2hpaHVhaHVhIG9yIHBhcnJvdC9ndWFjYW1vbGUgb3IgbGl2YXJvdC9wb250IGxldmVxdWUgb3Igd2hhdGV2ZXIvd2hhdGV2ZXIgZWxzZS4uLilfCgpXZSdyZSBnb2luZyB0byBidWlsZCBhICoqaG90ZG9nIC0gc2FsYWQgY2xhc3NpZmllcioqLiBBY3R1YWxseSB3ZSB3ZXJlIGluc3BpcmVkIGJ5IF9bU2lsaWNvbiBWYWxsZXldKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL1NpbGljb25fVmFsbGV5XyhUVl9zZXJpZXMpKV8ncyAiSG90ZG9nL05vdCBob3Rkb2cgY2xhc3NpZmllciIKCl8iV2hhdCB3b3VsZCB5b3Ugc2F5IGlmIEkgdG9sZCB5b3UgdGhlcmUgaXMgYSBhcHAgb24gdGhlIG1hcmtldCB0aGF0IHRlbGwgeW91IGlmIHlvdSBoYXZlIGEgaG90ZG9nIG9yIG5vdCBhIGhvdGRvZy4gSXQgaXMgdmVyeSBnb29kIGFuZCBJIGRvIG5vdCB3YW50IHRvIHdvcmsgb24gaXQgYW55IG1vcmUuIFlvdSBjYW4gaGlyZSBzb21lb25lIGVsc2UuIiAtIEppYW4tWWFuZywgU2lsaWNvbiBWYWxsZXksIDIwMTdfCgo8aWZyYW1lIHdpZHRoPSI0MjAiIGhlaWdodD0iMzE1IgpzcmM9Imh0dHBzOi8vd3d3LnlvdXR1YmUuY29tL2VtYmVkL0FDbXlkdEZEVEdzIj4KPC9pZnJhbWU+CgoKX2IpIGRvd25sb2FkIHNvbWUgKHNheSAxMCB0byA1MCkgaW1hZ2VzIG9mIGVhY2ggY2xhc3Mgb24gdGhlIHdlYiBzcGxpdCB5b3VyIGltYWdlcyBpbnRvIHR3byBzZXRzICh0cmFpbmluZyBhbmQgdGVzdGluZykgYW5kIHNldHVwIG91ciBkYXRhIHdpdGggYSB0cmFpbmluZyBkaXJlY3RvcnkgYW5kIGEgdmFsaWRhdGlvbiBkaXJlY3RvcnkgYXMgZm9sbG93czpfCgpXZSBkb3dubG9hZGVkIGZyb20gSW1hZ2VOZXQgYSBsaXN0IG9mIHVybHMgbGVhZGluZyB0byBwaWN0dXJlcyBvZiBzYWxhZHMgYW5kIGhvdGRvZ3MuCgpgYGB7cn0KZGlyLmNyZWF0ZSgic2FsYWQiKQpkaXIuY3JlYXRlKCJob3Rkb2ciKQpkb3dubG9hZC5maWxlKCJodHRwOi8vd3d3LmltYWdlLW5ldC5vcmcvYXBpL3RleHQvaW1hZ2VuZXQuc3luc2V0LmdldHVybHM/d25pZD1uMDc4MDY3NzQiLCJzYWxhZC9zYWxhZC50eHQiLCBtb2RlID0gJ3diJykKZG93bmxvYWQuZmlsZSgiaHR0cDovL3d3dy5pbWFnZS1uZXQub3JnL2FwaS90ZXh0L2ltYWdlbmV0LnN5bnNldC5nZXR1cmxzP3duaWQ9bjA3Njk3NTM3IiwiaG90ZG9nL2hvdGRvZy50eHQiLCBtb2RlID0gJ3diJykKYGBgCgpgYGB7cn0Kc2FsYWRfdXJsID0gcmVhZC50YWJsZSgic2FsYWQvc2FsYWQudHh0IikKc2FsYWRfdXJsJFYxPC0gYXMuY2hhcmFjdGVyKHNhbGFkX3VybCRWMSkKaG90ZG9nX3VybCA9IHJlYWQudGFibGUoImhvdGRvZy9ob3Rkb2cudHh0IikKaG90ZG9nX3VybCRWMTwtIGFzLmNoYXJhY3Rlcihob3Rkb2dfdXJsJFYxKQpgYGAKCgpMZXQncyBkb3dubG9hZCAzMDAgcGljdHVyZXMgb2Ygc2FsYWRzIGFuZCAzMDAgcGljdHVyZXMgb2YgaG90ZG9ncwpgYGB7cn0KZm9yIChpIGluIDE6MzAwKXsKICB0cnlDYXRjaCh7CiAgICBkb3dubG9hZC5maWxlKHNhbGFkX3VybFtpLF0scGFzdGUoInNhbGFkL3NhbGFkXyIsaSwiLmpwZyIsc2VwPSIiKSwgbW9kZSA9ICd3YicpCiAgICB9LAogICAgCiAgICBlcnJvcj1mdW5jdGlvbihlKXsKICAgICAgcHJpbnQoImVycm9yIikKICAgIH0pCn0KCmZvciAoaSBpbiAxOjMwMCl7CiAgdHJ5Q2F0Y2goewogICAgZG93bmxvYWQuZmlsZShob3Rkb2dfdXJsW2ksXSxwYXN0ZSgiaG90ZG9nL2hvdGRvZ18iLGksIi5qcGciLHNlcD0iIiksIG1vZGUgPSAnd2InKQogICAgfSwKICAgIAogICAgZXJyb3I9ZnVuY3Rpb24oZSl7CiAgICAgIHByaW50KCJlcnJvciIpCiAgICB9KQp9CiMKYGBgCgpTb21lIG9mIHRoZSBwaWN0dXJlcyB3ZXJlIGNvcnJ1cHRlZCBhbmQgc29tZSByZXByZXNlbnRlZCBtb3JlIHRoYW4gYSBzaW1wbGUgaXRlbSAoZS5nLiBhIHNhbGFkICYgYSBnbGFzcyBvZiB3YXRlciksIHNvIHdlIHJlbW92ZWQgdGhlbSBtYW51YWxseSBmcm9tIHRoZSBkYXRhc2V0LgoKQXQgdGhlIGVuZCBvZiB0aGUgZGF5IHdlIG9idGFpbiA6CmBgYHtyfQpmMSA9IGxpc3QuZmlsZXMoImhvdGRvZyIscGF0dGVybj0iLmpwZyIpCnByaW50KHBhc3RlKGxlbmd0aChmMSksICJob3Rkb2cgcGljdHVyZXMiKSkKZjIgPSBsaXN0LmZpbGVzKCJzYWxhZCIscGF0dGVybj0iLmpwZyIpCnByaW50KHBhc3RlKGxlbmd0aChmMiksICJzYWxhZCBwaWN0dXJlcyIpKQpgYGAKCkluIG9yZGVyIG5vdCB0byBmYXZvciBhbnkgY2F0ZWdvcnksIHdlJ3JlIGdvaW5nIHRvIGtlZXAgb25seSA2NCBzYWxhZCBwaWN0dXJlcwoKYGBge3J9CmYyPWYyWzE6bWluKGxlbmd0aChmMSksbGVuZ3RoKGYyKSldCmYxPWYxWzE6bWluKGxlbmd0aChmMSksbGVuZ3RoKGYyKSldCnByaW50KGxlbmd0aChmMik9PWxlbmd0aChmMSkpCnByaW50KGxlbmd0aChmMSkpCmBgYAoKCgpXZSBzcGxpdCBvdXIgZGF0YSBpbnRvIDgwLzIwIGFuZCBtb3ZlIGl0IHRvIHRyYWluL3Rlc3QgZGlyZWN0b3JpZXMuIFRoZSBhcmNoaXRlY3R1cmUgb2YgdGhlc2UgZGlyZWN0b3JpZXMgaXMgOgoKdHJhaW5fZGlyLwogIGhvdGRvZy8KICBzYWxhZC8KdGVzdF9kaXIvCiAgaG90ZG9nLwogIHNhbGFkLwoKYGBge3J9CmRpci5jcmVhdGUoInRyYWluX2RpciIpCmRpci5jcmVhdGUoInRyYWluX2Rpci9ob3Rkb2ciKQpkaXIuY3JlYXRlKCJ0cmFpbl9kaXIvc2FsYWQiKQpkaXIuY3JlYXRlKCJ0ZXN0X2RpciIpCmRpci5jcmVhdGUoInRlc3RfZGlyL2hvdGRvZyIpCmRpci5jcmVhdGUoInRlc3RfZGlyL3NhbGFkIikKYGBgCgoKYGBge3J9CmluZGV4X2hvdGRvZyA9IHNhbXBsZSgxOmxlbmd0aChmMSksIHJvdW5kKDAuOCpsZW5ndGgoZjEpKSkKCnRyYWluX2hvdGRvZyA9IGYxW2luZGV4X2hvdGRvZ10KZmlsZS5jb3B5KGZyb209cGFzdGUoImhvdGRvZy8iLHRyYWluX2hvdGRvZyxzZXA9IiIpLAogICAgICAgICAgdG89cGFzdGUoInRyYWluX2Rpci9ob3Rkb2cvIix0cmFpbl9ob3Rkb2csc2VwPSIiKSkKCnRlc3RfaG90ZG9nID0gZjFbLWluZGV4X2hvdGRvZ10KZmlsZS5jb3B5KGZyb209cGFzdGUoImhvdGRvZy8iLHRlc3RfaG90ZG9nLHNlcD0iIiksCiAgICAgICAgICB0bz1wYXN0ZSgidGVzdF9kaXIvaG90ZG9nLyIsdGVzdF9ob3Rkb2csc2VwPSIiKSkKCgppbmRleF9zYWxhZCA9IHNhbXBsZSgxOmxlbmd0aChmMiksIHJvdW5kKDAuOCpsZW5ndGgoZjIpKSkKCnRyYWluX3NhbGFkID0gZjJbaW5kZXhfc2FsYWRdCmZpbGUuY29weShmcm9tPXBhc3RlKCJzYWxhZC8iLHRyYWluX3NhbGFkLHNlcD0iIiksCiAgICAgICAgICB0bz1wYXN0ZSgidHJhaW5fZGlyL3NhbGFkLyIsdHJhaW5fc2FsYWQsc2VwPSIiKSkKCnRlc3Rfc2FsYWQgPSBmMlstaW5kZXhfc2FsYWRdCmZpbGUuY29weShmcm9tPXBhc3RlKCJzYWxhZC8iLHRlc3Rfc2FsYWQsc2VwPSIiKSwKICAgICAgICAgIHRvPXBhc3RlKCJ0ZXN0X2Rpci9zYWxhZC8iLHRlc3Rfc2FsYWQsc2VwPSIiKSkKIwpgYGAKCl9jKSB0cmFpbmluZyB0aGUgbmV0d29ya18KCldlIGluaXRpYWxseSB0cmllZCB0byB1c2UgYW4gWGNlcHRpb24gcHJlLXRyYWluZWQgbmV1cmFsIG5ldCBpbiBvcmRlciB0byBidWlsZCBvdXIgbW9kZWwsIGJ1dCB3ZSBmYWlsIHRvIHR1bmUgdGhlIGh5cGVycGFyYW1ldGVycyAod2UgYWxzbyB0cmllZCB0byBhZGQgbW9yZSBkYXRhIGFuZCB0cnkgdG8gW2F1Z21lbnQgaXRdKGh0dHBzOi8vdG93YXJkc2RhdGFzY2llbmNlLmNvbS9pbWFnZS1hdWdtZW50YXRpb24tZm9yLWRlZXAtbGVhcm5pbmctaGlzdG9ncmFtLWVxdWFsaXphdGlvbi1hNzEzODdmNjA5YjIpKSAuLi4gVGhlc2UgYXJlIHRoZSBmaW5hbCByZXN1bHRzIHdlIG9idGFpbmVkIHdpdGggWGNlcHRpb24gOgo8ZGl2IGlkPSJiZyI+CiAgPGltZyBzcmM9IkltZy9YY2VwdGlvbi5wbmciIHdpZHRoPTQwMD4KPC9kaXY+IAoKU28gd2UgbW92ZWQgdG8gVkdHIDE2IDoKCmBgYHtyfQoKIyMjIERlZmluaW5nIHRyYWluaW5nIGFuZCB0ZXN0aW5nIHNldHMKCmYxID0gbGlzdC5maWxlcygiaG90ZG9nIixwYXR0ZXJuPSIuanBnIikKZjIgPSBsaXN0LmZpbGVzKCJzYWxhZCIscGF0dGVybj0iLmpwZyIpCgppbWdfd2lkdGggPSAxNTAKaW1nX2hlaWdodCA9IDE1MApiYXRjaF9zaXplID0gMTAKCnRyYWluX2RpcmVjdG9yeSA9ICJ0cmFpbl9kaXIiCnRlc3RfZGlyZWN0b3J5ID0gInRlc3RfZGlyIgogCnRyYWluX2dlbmVyYXRvciA9IGZsb3dfaW1hZ2VzX2Zyb21fZGlyZWN0b3J5KAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbl9kaXJlY3RvcnksIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBnZW5lcmF0b3IgPSBpbWFnZV9kYXRhX2dlbmVyYXRvcigpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0YXJnZXRfc2l6ZSA9IGMoaW1nX3dpZHRoLCBpbWdfaGVpZ2h0KSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29sb3JfbW9kZSA9ICJyZ2IiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc19tb2RlID0gImNhdGVnb3JpY2FsIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJhdGNoX3NpemUgPSBiYXRjaF9zaXplLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaHVmZmxlPVRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSA0MiApCgoKdmFsaWRhdGlvbl9nZW5lcmF0b3IgPSBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeSgKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9kaXJlY3RvcnksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdlbmVyYXRvciA9IGltYWdlX2RhdGFfZ2VuZXJhdG9yKCksICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGFyZ2V0X3NpemUgPSBjKGltZ193aWR0aCwgaW1nX2hlaWdodCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbG9yX21vZGUgPSAicmdiIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NlcyA9IE5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzX21vZGUgPSAiY2F0ZWdvcmljYWwiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYXRjaF9zaXplID0gYmF0Y2hfc2l6ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2h1ZmZsZSA9IFRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSA0MikKCnRyYWluX3NhbXBsZXMgPSByb3VuZCgwLjgqbGVuZ3RoKGYxKSkKdmFsaWRhdGlvbl9zYW1wbGVzID0gcm91bmQoMC4yKmxlbmd0aChmMSkpCgoKIyMjIERlZmluaW5nIHRoZSBwcmV0cmFpbmVkIG1vZGVsIDogVkdHMTYKCmJhc2VfbW9kZWwgPC0gYXBwbGljYXRpb25fdmdnMTYod2VpZ2h0cyA9ICdpbWFnZW5ldCcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5jbHVkZV90b3AgPSBGQUxTRSwgIyMgV2UgZHJvcCB0aGUgZmluYWwgbGF5ZXIgKGkuZS4gdGhlIHByZWRpY3Rpb24gbGF5ZXIpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW5wdXRfc2hhcGUgPSBjKGltZ193aWR0aCwgaW1nX2hlaWdodCwgMykpCiAKCiMjIyBBZGRpbmcgb25lIG1vcmUgbGF5ZXIKCnByZWRpY3Rpb25zIDwtIGJhc2VfbW9kZWwkb3V0cHV0ICU+JSAKICAgICAgICAgICAgICAgICAgICAgIGxheWVyX2dsb2JhbF9hdmVyYWdlX3Bvb2xpbmdfMmQodHJhaW5hYmxlID0gVCkgJT4lIAogICAgICAgICAgICAgICAgICAgICAgbGF5ZXJfZGVuc2UoMTI4LCB0cmFpbmFibGUgPSBUKSAlPiUgIzEyOCBmdWxseSBjb25uZWN0ZWQgbmV1cm9ucwogICAgICAgICAgICAgICAgICAgICAgbGF5ZXJfYWN0aXZhdGlvbigicmVsdSIsIHRyYWluYWJsZSA9IFQpICU+JSAjUmVMVSBhY3RpdmF0aW9uIGZ1bmN0aW9uCiAgICAgICAgICAgICAgICAgICAgICBsYXllcl9kcm9wb3V0KDAuNSwgdHJhaW5hYmxlID0gVCkgJT4lICNEcm9wcGluZy1vdXQgNTAlIG9mIHRoZSAgaW50ZXJtZWRpYXJ5IHJlc3VsdHMgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICN0byBhdm9pZCBvdmVyZml0dGluZwogICAgICAgICAgICAgICAgICAgICAgbGF5ZXJfZGVuc2UoMiwgdHJhaW5hYmxlPVQpICU+JSAjIFdlIGFkZCAxIG1vcmUgbGF5ZXIgd2l0aCBvbmx5IDIgbmV1cm9zID0gMiBjbGFzc2VzCiAgICAgICAgICAgICAgICAgICAgICBsYXllcl9hY3RpdmF0aW9uKCJzb2Z0bWF4IiwgdHJhaW5hYmxlPVQpICMgVGhlIG91dF9wdXQgZnVuY3Rpb24gaXMgc29mdG1heAogCgojIyMgVGhlIG1vZGVsIHRvIHRyYWluCgptb2RlbCA8LSBrZXJhc19tb2RlbChpbnB1dHMgPSBiYXNlX21vZGVsJGlucHV0LCBvdXRwdXRzID0gcHJlZGljdGlvbnMpCgoKIyMjIEZyZWV6aW5nIGFsbCBsYXllcnMgcHJldmlvdXNseSB0cmFpbmVkIGJ5IFZHRzE2Cgpmb3IgKGxheWVyIGluIGJhc2VfbW9kZWwkbGF5ZXJzKXsKICBsYXllciR0cmFpbmFibGUgPC0gRkFMU0UKfQogIAogCiMjIyBMZXQncyBkbyB0aGUgbWF0aHMKCm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAiY2F0ZWdvcmljYWxfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMDA1LCBkZWNheSA9IDFlLTYpLCAjbGVhcm5pbmcgcmF0ZSBvZiAwLjAwNQogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKIApyZXN1bHRzIDwtIG1vZGVsICU+JSBmaXRfZ2VuZXJhdG9yKAogICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX2dlbmVyYXRvciwKICAgICAgICAgICAgICAgICAgICAgICAgICBzdGVwc19wZXJfZXBvY2ggPSBhcy5pbnRlZ2VyKHRyYWluX3NhbXBsZXMvYmF0Y2hfc2l6ZSksIAogICAgICAgICAgICAgICAgICAgICAgICAgIGVwb2NocyA9IDIwLCAjIDIwIGVwb2NocwogICAgICAgICAgICAgICAgICAgICAgICAgIHZhbGlkYXRpb25fZGF0YSA9IHZhbGlkYXRpb25fZ2VuZXJhdG9yLAogICAgICAgICAgICAgICAgICAgICAgICAgIHZhbGlkYXRpb25fc3RlcHMgPSBhcy5pbnRlZ2VyKHZhbGlkYXRpb25fc2FtcGxlcy9iYXRjaF9zaXplKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlPTIKKQoKa2VyYXM6OnNhdmVfbW9kZWxfaGRmNShtb2RlbCwibW9kZWwuaGRmNSIpCmBgYAoKCkhlcmUgYXJlIHRoZSByZXN1bHRzIHdlIG9idGFpbmVkLiBBdCB0aGUgYmVzdCBvZiB0aGUgbGVhcm5pbmcgcHJvY2VzcyB3ZSBhY2hpZXZlZCAwLjk2IG9mIGFjY3VyYWN5IGFuZCAwLjk3IG9mIHZhbGlkYXRpb24gYWNjdXJhY3kuCgpgYGB7cn0KcGxvdChyZXN1bHRzKQpgYGAKCgoqKkJvbnVzOioqIFRoaXMgaXMgdGhlIGNodW5rIG9mIGNvZGUgd2UgdXNlZCB0byBhdWdtZW50IHRoZSBkYXRhIHdoZW4gWGNlcHRpb24gd291bGRuJ3Qgd29yayAuLi4KYGBge3J9CgojIyMgR2VuZXJhdGluZyAibmV3IiBkYXRhIG91dCBvZiB0aGUgb3JpZ2luYWwgZGF0YSBieSByb3RhdGluZy90cmFuc2xhdGluZy9jcm9waW5nL3Jlc2l6aW5nLy4uLiBpdAoKZGF0YWdlbiA8LSBpbWFnZV9kYXRhX2dlbmVyYXRvcigKICByb3RhdGlvbl9yYW5nZSA9IDIwLAogIHdpZHRoX3NoaWZ0X3JhbmdlID0gMC4yLAogIGhlaWdodF9zaGlmdF9yYW5nZSA9IDAuMiwKICBob3Jpem9udGFsX2ZsaXAgPSBUUlVFCikKIAp0cmFpbl9hdWdtZW50ZWRfZ2VuZXJhdG9yIDwtICBmbG93X2ltYWdlc19mcm9tX2RpcmVjdG9yeSh0ZXN0X2RpcmVjdG9yeSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdlbmVyYXRvciA9IGRhdGFnZW4sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldF9zaXplID0gYyhpbWdfd2lkdGgsIGltZ19oZWlnaHQpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29sb3JfbW9kZSA9ICJyZ2IiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NlcyA9IE5VTEwsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc19tb2RlID0gImNhdGVnb3JpY2FsIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJhdGNoX3NpemUgPSBiYXRjaF9zaXplLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2h1ZmZsZSA9IFRSVUUsICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZCA9IDQyKQoKIyMjIFRoZSBtb2RlbCB0byB0cmFpbgoKbW9kZWwgPC0ga2VyYXNfbW9kZWwoaW5wdXRzID0gYmFzZV9tb2RlbCRpbnB1dCwgb3V0cHV0cyA9IHByZWRpY3Rpb25zKQoKZm9yIChsYXllciBpbiBiYXNlX21vZGVsJGxheWVycyl7CiAgbGF5ZXIkdHJhaW5hYmxlIDwtIEZBTFNFCn0KCgojIyMgVHJhaW5pbmcgb24gdGhlIGF1Z21lbnRlZCBkYXRhCgpyZXN1bHRfYXVnIDwtIG1vZGVsICU+JSBmaXRfZ2VuZXJhdG9yKAogIHRyYWluX2F1Z21lbnRlZF9nZW5lcmF0b3IsCiAgc3RlcHNfcGVyX2Vwb2NoID0gYXMuaW50ZWdlcih0cmFpbl9zYW1wbGVzL2JhdGNoX3NpemUpLCAKICBlcG9jaHMgPSAyMCwgCiAgdmFsaWRhdGlvbl9kYXRhID0gdmFsaWRhdGlvbl9nZW5lcmF0b3IsCiAgdmFsaWRhdGlvbl9zdGVwcyA9IGFzLmludGVnZXIodmFsaWRhdGlvbl9zYW1wbGVzL2JhdGNoX3NpemUpLAogIHZlcmJvc2U9MgopCmBgYAoKCgoqKkxldCdzIHZpc3VhbGl6ZSB0aGUgcmVzdWx0cyBvZiBvdXIgbW9kZWwqKgoKV2UgdGVzdCBvdXIgbW9kZWwgb24gOgoKKiBBIHJlZ3VsYXIgaG90ZG9nCiogQSByZWd1bGFyIHNhbGFkCiogU25hcGNoYXQncyBkYW5jaW5nIGhvdGRvZwoqIEEgc2FsYWQgd2l0aCBhIGhvdGRvZwoKCjxpZnJhbWUgd2lkdGg9IjQyMCIgaGVpZ2h0PSIzMTUiCnNyYz0iaHR0cHM6Ly93d3cueW91dHViZS5jb20vZW1iZWQvc19Ma3pVNWdEWkkiPgo8L2lmcmFtZT4KCgoKYGBge3J9CmltZ19saXN0ID0gYygiaG90ZG9nLnBuZyIsInNhbGFkLmpwZyIsInNuYXAucG5nIiwiaG90ZG9nX3NhbGFkLmpwZyIpCmltZ19wYXRoX2xpc3QgPSBwYXN0ZSgiSW1nLyIsaW1nX2xpc3Qsc2VwPSIiKQoKZm9yIChpbWdfcGF0aCBpbiBpbWdfcGF0aF9saXN0KXsKICBpbWcgPC0gaW1hZ2VfbG9hZChpbWdfcGF0aCwgdGFyZ2V0X3NpemUgPSBjKDE1MCwxNTApKQogIHBsb3QobG9hZC5pbWFnZShpbWdfcGF0aCkpCiAgeCA8LSBpbWFnZV90b19hcnJheShpbWcpCiAgeCA8LSBhcnJheV9yZXNoYXBlKHgsIGMoMSwgZGltKHgpKSkKICB4IDwtIGltYWdlbmV0X3ByZXByb2Nlc3NfaW5wdXQoeCkKICBwcmVkcyA8LSBtb2RlbCAlPiUgcHJlZGljdCh4KQogIGlmIChwcmVkc1ssMV09PTEpewogICAgcHJpbnQoIkhvdGRvZyIpCiAgfSBlbHNlIHsKICAgIHByaW50KCJTYWxhZCIpCiAgfQp9CmBgYAoKQXMgZXhwZWN0ZWQgd2UgcHJlZGljdGVkIHdlbGwgb24gcmVndWxhciB1c2VjYXNlcy4gWWV0IGluIHRoZSB0cmlja3kgY2FzZSBvZiBhIHNhbGFkICsgYSBob3Rkb2cgb3VyIGNsYXNzaWZpZXIgcHJlZGljdGVkIGEgc2FsYWQgcmF0aGVyIHRoYW4gYSBob3Rkb2cuCgoKCjIuIElzIGl0IGJldHRlciB0byBkbyB0cmFuc2ZlciBsZWFybmluZyBhbmQgZmluZSB0dW5pbmcgb3IgYm90aD8KCldlIG9idGFpbmVkIHRoZXNlIGZldyBndWlkZWxpbmVzIGZyb20gW0RlZXAgTGVhcm5pbmcgU2FuZGJveF0oaHR0cHM6Ly9kZWVwbGVhcm5pbmdzYW5kYm94LmNvbS9ob3ctdG8tdXNlLXRyYW5zZmVyLWxlYXJuaW5nLWFuZC1maW5lLXR1bmluZy1pbi1rZXJhcy1hbmQtdGVuc29yZmxvdy10by1idWlsZC1hbi1pbWFnZS1yZWNvZ25pdGlvbi05NGIwYjAyNDQ0ZjIpCgoKKiAqKlNpbWlsYXIgJiBzbWFsbCBkYXRhc2V0OioqIGF2b2lkIG92ZXJmaXR0aW5nIGJ5IG5vdCBmaW5lLXR1bmluZyB0aGUgd2VpZ2h0cyBvbiBhIHNtYWxsIGRhdGFzZXQsIGFuZCB1c2UgZXh0cmFjdGVkIGZlYXR1cmVzIGZyb20gdGhlIGhpZ2hlc3QgbGV2ZWxzIG9mIHRoZSBDb252TmV0IHRvIGxldmVyYWdlIGRhdGFzZXQgc2ltaWxhcml0eS4KKiAqKkRpZmZlcmVudCAmIHNtYWxsIGRhdGFzZXQ6KiogYXZvaWQgb3ZlcmZpdHRpbmcgYnkgbm90IGZpbmUtdHVuaW5nIHRoZSB3ZWlnaHRzIG9uIGEgc21hbGwgZGF0YXNldCwgYW5kIHVzZSBleHRyYWN0ZWQgZmVhdHVyZXMgZnJvbSBsb3dlciBsZXZlbHMgb2YgdGhlIENvbnZOZXQgd2hpY2ggYXJlIG1vcmUgZ2VuZXJhbGl6YWJsZS4KKiAqKlNpbWlsYXIgJiBsYXJnZSBkYXRhc2V0OioqIHdpdGggYSBsYXJnZSBkYXRhc2V0IHdlIGNhbiBmaW5lLXR1bmUgdGhlIHdlaWdodHMgd2l0aCBsZXNzIG9mIGEgY2hhbmNlIHRvIG92ZXJmaXQgdGhlIHRyYWluaW5nIGRhdGEuCiogKipEaWZmZXJlbnQgJiBsYXJnZSBkYXRhc2V0OioqIHdpdGggYSBsYXJnZSBkYXRhc2V0IHdlIGFnYWluIGNhbiBmaW5lLXR1bmUgdGhlIHdlaWdodHMgd2l0aCBsZXNzIG9mIGEgY2hhbmNlIHRvIG92ZXJmaXQuCgoKCjxkaXYgaWQ9ImJnIj4KICA8aW1nIHNyYz0iSW1nL01hcC5wbmciIHdpZHRoPTUwMD4KPC9kaXY+IAoKCiMgT24gb3VyIHdheSB0byBtYWtlIG1vbmV5ICEKPGlmcmFtZSB3aWR0aD0iNDIwIiBoZWlnaHQ9IjMxNSIKc3JjPSJodHRwczovL3d3dy55b3V0dWJlLmNvbS9lbWJlZC9BSnNPQTRabDZJbyI+CjwvaWZyYW1lPgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg==